<?php

namespace Luracast\Restler;

use Exception;
use InvalidArgumentException;
use Luracast\Restler\Data\ApiMethodInfo;
use Luracast\Restler\Data\ValidationInfo;
use Luracast\Restler\Data\Validator;
use Luracast\Restler\Format\iFormat;
use Luracast\Restler\Format\iDecodeStream;
use Luracast\Restler\Format\UrlEncodedFormat;

/**
 * REST API Server.
 * It is the server part of the Restler framework.
 * inspired by the RestServer code from
 * <http://jacwright.com/blog/resources/RestServer.txt>
 *
 * @category Framework
 * @package Restler
 * @author R.Arul Kumaran <arul@luracast.com>
 * @copyright 2010 Luracast
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link http://luracast.com/products/restler/
 * @version 3.0.0rc5
 */
class Restler extends EventDispatcher {
	const VERSION = '3.0.0rc5';
	
	// ==================================================================
	//
	// Public variables
	//
	// ------------------------------------------------------------------
	/**
	 * Reference to the last exception thrown
	 * 
	 * @var RestException
	 */
	public $exception = null;
	/**
	 * Used in production mode to store the routes and more
	 *
	 * @var iCache
	 */
	public $cache;
	/**
	 * URL of the currently mapped service
	 *
	 * @var string
	 */
	public $url;
	/**
	 * Http request method of the current request.
	 * Any value between [GET, PUT, POST, DELETE]
	 *
	 * @var string
	 */
	public $requestMethod;
	/**
	 * Requested data format.
	 * Instance of the current format class
	 * which implements the iFormat interface
	 *
	 * @var iFormat
	 * @example jsonFormat, xmlFormat, yamlFormat etc
	 */
	public $requestFormat;
	/**
	 * Response data format.
	 *
	 * Instance of the current format class
	 * which implements the iFormat interface
	 *
	 * @var iFormat
	 * @example jsonFormat, xmlFormat, yamlFormat etc
	 */
	public $responseFormat;
	/**
	 * Http status code
	 *
	 * @var int
	 */
	public $responseCode = 200;
	/**
	 *
	 * @var string base url of the api service
	 */
	protected $baseUrl;
	/**
	 *
	 * @var bool Used for waiting till verifying @format
	 *      before throwing content negotiation failed
	 */
	protected $requestFormatDiffered = false;
	/**
	 * method information including metadata
	 *
	 * @var ApiMethodInfo
	 */
	public $apiMethodInfo;
	/**
	 *
	 * @var int for calculating execution time
	 */
	protected $startTime;
	/**
	 * When set to false, it will run in debug mode and parse the
	 * class files every time to map it to the URL
	 *
	 * @var boolean
	 */
	protected $productionMode = false;
	public $refreshCache = false;
	/**
	 * Caching of url map is enabled or not
	 *
	 * @var boolean
	 */
	protected $cached;
	/**
	 *
	 * @var int
	 */
	protected $apiVersion = 1;
	/**
	 *
	 * @var int
	 */
	protected $requestedApiVersion = 1;
	/**
	 *
	 * @var int
	 */
	protected $apiMinimumVersion = 1;
	/**
	 *
	 * @var array
	 */
	protected $apiVersionMap = array ();
	/**
	 * Associated array that maps formats to their respective format class name
	 *
	 * @var array
	 */
	protected $formatMap = array ();
	/**
	 * List of the Mime Types that can be produced as a response by this API
	 *
	 * @var array
	 */
	protected $writableMimeTypes = array ();
	/**
	 * List of the Mime Types that are supported for incoming requests by this API
	 *
	 * @var array
	 */
	protected $readableMimeTypes = array ();
	/**
	 * Associated array that maps formats to their respective format class name
	 *
	 * @var array
	 */
	protected $formatOverridesMap = array (
			'extensions' => array () 
	);
	/**
	 * list of filter classes
	 *
	 * @var array
	 */
	protected $filterClasses = array ();
	/**
	 * instances of filter classes that are executed after authentication
	 *
	 * @var array
	 */
	protected $postAuthFilterClasses = array ();
	
	// ==================================================================
	//
	// Protected variables
	//
	// ------------------------------------------------------------------
	
	/**
	 * Data sent to the service
	 *
	 * @var array
	 */
	protected $requestData = array ();
	/**
	 * list of authentication classes
	 *
	 * @var array
	 */
	protected $authClasses = array ();
	/**
	 * list of error handling classes
	 *
	 * @var array
	 */
	protected $errorClasses = array ();
	protected $authenticated = false;
	protected $authVerified = false;
	/**
	 *
	 * @var mixed
	 */
	protected $responseData;
	
	/**
	 * Constructor
	 *
	 * @param boolean $productionMode
	 *        	When set to false, it will run in
	 *        	debug mode and parse the class files
	 *        	every time to map it to the URL
	 *        	
	 * @param bool $refreshCache
	 *        	will update the cache when set to true
	 */
	public function __construct($productionMode = false, $refreshCache = false) {
		parent::__construct ();
		$this->startTime = time ();
		Util::$restler = $this;
		Scope::set ( 'Restler', $this );
		$this->productionMode = $productionMode;
		if (is_null ( Defaults::$cacheDirectory )) {
			Defaults::$cacheDirectory = dirname ( $_SERVER ['SCRIPT_FILENAME'] ) . DIRECTORY_SEPARATOR . 'cache';
		}
		$this->cache = new Defaults::$cacheClass ();
		$this->refreshCache = $refreshCache;
		// use this to rebuild cache every time in production mode
		if ($productionMode && $refreshCache) {
			$this->cached = false;
		}
	}
	
	/**
	 * Main function for processing the api request
	 * and return the response
	 *
	 * @throws Exception when the api service class is missing
	 * @throws RestException to send error response
	 */
	public function handle() {
		try {
			try {
				try {
					$this->get ();
				} catch ( Exception $e ) {
					$this->requestData = array (
							Defaults::$fullRequestDataName => array () 
					);
					if (! $e instanceof RestException) {
						$e = new RestException ( 500, $this->productionMode ? null : $e->getMessage (), array (), $e );
					}
					$this->route ();
					throw $e;
				}
				if (Defaults::$useVendorMIMEVersioning)
					$this->responseFormat = $this->negotiateResponseFormat ();
				$this->route ();
			} catch ( Exception $e ) {
				$this->negotiate ();
				if (! $e instanceof RestException) {
					$e = new RestException ( 500, $this->productionMode ? null : $e->getMessage (), array (), $e );
				}
				throw $e;
			}
			$this->negotiate ();
			$this->preAuthFilter ();
			$this->authenticate ();
			$this->postAuthFilter ();
			$this->validate ();
			$this->preCall ();
			$this->call ();
			$this->compose ();
			$this->postCall ();
			$this->respond ();
		} catch ( Exception $e ) {
			try {
				$this->message ( $e );
			} catch ( Exception $e2 ) {
				$this->message ( $e2 );
			}
		}
	}
	
	/**
	 * read the request details
	 *
	 * Find out the following
	 * - baseUrl
	 * - url requested
	 * - version requested (if url based versioning)
	 * - http verb/method
	 * - negotiate content type
	 * - request data
	 * - set defaults
	 */
	protected function get() {
		$this->dispatch ( 'get' );
		if (empty ( $this->formatMap )) {
			$this->setSupportedFormats ( 'JsonFormat' );
		}
		$this->url = $this->getPath ();
		$this->requestMethod = Util::getRequestMethod ();
		$this->requestFormat = $this->getRequestFormat ();
		$this->requestData = $this->getRequestData ( false );
		
		// parse defaults
		foreach ( $_GET as $key => $value ) {
			if (isset ( Defaults::$aliases [$key] )) {
				$_GET [Defaults::$aliases [$key]] = $value;
				unset ( $_GET [$key] );
				$key = Defaults::$aliases [$key];
			}
			if (in_array ( $key, Defaults::$overridables )) {
				Defaults::setProperty ( $key, $value );
			}
		}
	}
	
	/**
	 * Returns a list of the mime types (e.g.
	 * ["application/json","application/xml"]) that the API can respond with
	 * 
	 * @return array
	 */
	public function getWritableMimeTypes() {
		return $this->writableMimeTypes;
	}
	
	/**
	 * Returns the list of Mime Types for the request that the API can understand
	 * 
	 * @return array
	 */
	public function getReadableMimeTypes() {
		return $this->readableMimeTypes;
	}
	
	/**
	 * Call this method and pass all the formats that should be supported by
	 * the API Server.
	 * Accepts multiple parameters
	 *
	 * @param
	 *        	string ,... $formatName class name of the format class that
	 *        	implements iFormat
	 *        	
	 * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
	 * @throws Exception
	 */
	public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/)
    {
		$args = func_get_args ();
		$extensions = array ();
		$throwException = $this->requestFormatDiffered;
		$this->writableMimeTypes = $this->readableMimeTypes = array ();
		foreach ( $args as $className ) {
			
			$obj = Scope::get ( $className );
			
			if (! $obj instanceof iFormat)
				throw new Exception ( 'Invalid format class; must implement ' . 'iFormat interface' );
			if ($throwException && get_class ( $obj ) == get_class ( $this->requestFormat )) {
				$throwException = false;
			}
			
			foreach ( $obj->getMIMEMap () as $mime => $extension ) {
				if ($obj->isWritable ()) {
					$this->writableMimeTypes [] = $mime;
					$extensions [".$extension"] = true;
				}
				if ($obj->isReadable ())
					$this->readableMimeTypes [] = $mime;
				if (! isset ( $this->formatMap [$extension] ))
					$this->formatMap [$extension] = $className;
				if (! isset ( $this->formatMap [$mime] ))
					$this->formatMap [$mime] = $className;
			}
		}
		if ($throwException) {
			throw new RestException ( 403, 'Content type `' . $this->requestFormat->getMIME () . '` is not supported.' );
		}
		$this->formatMap ['default'] = $args [0];
		$this->formatMap ['extensions'] = array_keys ( $extensions );
	}
	
	/**
	 * Call this method and pass all the formats that can be used to override
	 * the supported formats using `@format` comment.
	 * Accepts multiple parameters
	 *
	 * @param
	 *        	string ,... $formatName class name of the format class that
	 *        	implements iFormat
	 *        	
	 * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...);
	 * @throws Exception
	 */
	public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/)
    {
		$args = func_get_args ();
		$extensions = array ();
		foreach ( $args as $className ) {
			
			$obj = Scope::get ( $className );
			
			if (! $obj instanceof iFormat)
				throw new Exception ( 'Invalid format class; must implement ' . 'iFormat interface' );
			
			foreach ( $obj->getMIMEMap () as $mime => $extension ) {
				if (! isset ( $this->formatOverridesMap [$extension] ))
					$this->formatOverridesMap [$extension] = $className;
				if (! isset ( $this->formatOverridesMap [$mime] ))
					$this->formatOverridesMap [$mime] = $className;
				if ($obj->isWritable ())
					$extensions [".$extension"] = true;
			}
		}
		$this->formatOverridesMap ['extensions'] = array_keys ( $extensions );
	}
	
	/**
	 * Parses the request url and get the api path
	 *
	 * @return string api path
	 */
	protected function getPath() {
		// fix SCRIPT_NAME for PHP 5.4 built-in web server
		if (false === strpos ( $_SERVER ['SCRIPT_NAME'], '.php' ))
			$_SERVER ['SCRIPT_NAME'] = '/' . Util::removeCommonPath ( $_SERVER ['SCRIPT_FILENAME'], $_SERVER ['DOCUMENT_ROOT'] );
		
		$fullPath = urldecode ( $_SERVER ['REQUEST_URI'] );
		$path = Util::removeCommonPath ( $fullPath, $_SERVER ['SCRIPT_NAME'] );
		$port = isset ( $_SERVER ['SERVER_PORT'] ) ? $_SERVER ['SERVER_PORT'] : '80';
		$https = $port == '443' || (isset ( $_SERVER ['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER ['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB
(isset ( $_SERVER ['HTTPS'] ) && $_SERVER ['HTTPS'] == 'on');
		
		$baseUrl = ($https ? 'https://' : 'http://') . $_SERVER ['SERVER_NAME'];
		
		if (! $https && $port != '80' || $https && $port != '443')
			$baseUrl .= ':' . $port;
		
		$this->baseUrl = rtrim ( $baseUrl . substr ( $fullPath, 0, strlen ( $fullPath ) - strlen ( $path ) ), '/' );
		
		$path = rtrim ( strtok ( $path, '?' ), '/' ); // remove query string and trailing slash if found any
		$path = str_replace ( array_merge ( $this->formatMap ['extensions'], $this->formatOverridesMap ['extensions'] ), '', $path );
		if (Defaults::$useUrlBasedVersioning && strlen ( $path ) && $path {0} == 'v') {
			$version = intval ( substr ( $path, 1 ) );
			if ($version && $version <= $this->apiVersion) {
				$this->requestedApiVersion = $version;
				$path = explode ( '/', $path, 2 );
				$path = $path [1];
			}
		} else {
			$this->requestedApiVersion = $this->apiMinimumVersion;
		}
		return $path;
	}
	
	/**
	 * Parses the request to figure out format of the request data
	 *
	 * @throws RestException
	 * @return iFormat any class that implements iFormat
	 * @example JsonFormat
	 */
	protected function getRequestFormat() {
		$format = null;
		// check if client has sent any information on request format
		if (! empty ( $_SERVER ['CONTENT_TYPE'] ) || (! empty ( $_SERVER ['HTTP_CONTENT_TYPE'] ) && $_SERVER ['CONTENT_TYPE'] = $_SERVER ['HTTP_CONTENT_TYPE'])) {
			$mime = $_SERVER ['CONTENT_TYPE'];
			if (false !== $pos = strpos ( $mime, ';' )) {
				$mime = substr ( $mime, 0, $pos );
			}
			if ($mime == UrlEncodedFormat::MIME)
				$format = Scope::get ( 'UrlEncodedFormat' );
			elseif (isset ( $this->formatMap [$mime] )) {
				$format = Scope::get ( $this->formatMap [$mime] );
				$format->setMIME ( $mime );
			} elseif (! $this->requestFormatDiffered && isset ( $this->formatOverridesMap [$mime] )) {
				// if our api method is not using an @format comment
				// to point to this $mime, we need to throw 403 as in below
				// but since we don't know that yet, we need to defer that here
				$format = Scope::get ( $this->formatOverridesMap [$mime] );
				$format->setMIME ( $mime );
				$this->requestFormatDiffered = true;
			} else {
				throw new RestException ( 403, "Content type `$mime` is not supported." );
			}
		}
		if (! $format) {
			$format = Scope::get ( $this->formatMap ['default'] );
		}
		return $format;
	}
	public function getRequestStream() {
		static $tempStream = false;
		if (! $tempStream) {
			$tempStream = fopen ( 'php://temp', 'r+' );
			$rawInput = fopen ( 'php://input', 'r' );
			stream_copy_to_stream ( $rawInput, $tempStream );
		}
		rewind ( $tempStream );
		return $tempStream;
	}
	
	/**
	 * Parses the request data and returns it
	 *
	 * @param bool $includeQueryParameters        	
	 *
	 * @return array php data
	 */
	public function getRequestData($includeQueryParameters = true) {
		$get = UrlEncodedFormat::decoderTypeFix ( $_GET );
		if ($this->requestMethod == 'PUT' || $this->requestMethod == 'PATCH' || $this->requestMethod == 'POST') {
			if (! empty ( $this->requestData )) {
				return $includeQueryParameters ? $this->requestData + $get : $this->requestData;
			}
			
			$stream = $this->getRequestStream ();
			if ($stream === FALSE)
				return array ();
			$r = $this->requestFormat instanceof iDecodeStream ? $this->requestFormat->decodeStream ( $stream ) : $this->requestFormat->decode ( stream_get_contents ( $stream ) );
			
			$r = is_array ( $r ) ? array_merge ( $r, array (
					Defaults::$fullRequestDataName => $r 
			) ) : array (
					Defaults::$fullRequestDataName => $r 
			);
			return $includeQueryParameters ? $r + $get : $r;
		}
		return $includeQueryParameters ? $get : array (); // no body
	}
	
	/**
	 * Find the api method to execute for the requested Url
	 */
	protected function route() {
		$this->dispatch ( 'route' );
		
		$params = $this->getRequestData ();
		
		// backward compatibility for restler 2 and below
		if (! Defaults::$smartParameterParsing) {
			$params = $params + array (
					Defaults::$fullRequestDataName => $params 
			);
		}
		
		$this->apiMethodInfo = $o = Routes::find ( $this->url, $this->requestMethod, $this->requestedApiVersion, $params );
		// set defaults based on api method comments
		if (isset ( $o->metadata )) {
			foreach ( Defaults::$fromComments as $key => $defaultsKey ) {
				if (array_key_exists ( $key, $o->metadata )) {
					$value = $o->metadata [$key];
					Defaults::setProperty ( $defaultsKey, $value );
				}
			}
		}
		if (! isset ( $o->className ))
			throw new RestException ( 404 );
		
		if (isset ( $this->apiVersionMap [$o->className] )) {
			Scope::$classAliases [Util::getShortName ( $o->className )] = $this->apiVersionMap [$o->className] [$this->requestedApiVersion];
		}
		
		foreach ( $this->authClasses as $auth ) {
			if (isset ( $this->apiVersionMap [$auth] )) {
				Scope::$classAliases [$auth] = $this->apiVersionMap [$auth] [$this->requestedApiVersion];
			} elseif (isset ( $this->apiVersionMap [Scope::$classAliases [$auth]] )) {
				Scope::$classAliases [$auth] = $this->apiVersionMap [Scope::$classAliases [$auth]] [$this->requestedApiVersion];
			}
		}
	}
	
	/**
	 * Negotiate the response details such as
	 * - cross origin resource sharing
	 * - media type
	 * - charset
	 * - language
	 */
	protected function negotiate() {
		$this->dispatch ( 'negotiate' );
		$this->negotiateCORS ();
		$this->responseFormat = $this->negotiateResponseFormat ();
		$this->negotiateCharset ();
		$this->negotiateLanguage ();
	}
	protected function negotiateCORS() {
		if ($this->requestMethod == 'OPTIONS' && Defaults::$crossOriginResourceSharing) {
			if (isset ( $_SERVER ['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ))
				header ( 'Access-Control-Allow-Methods: ' . Defaults::$accessControlAllowMethods );
			
			if (isset ( $_SERVER ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ))
				header ( 'Access-Control-Allow-Headers: ' . $_SERVER ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] );
			
			header ( 'Access-Control-Allow-Origin: ' . (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER ['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin) );
			header ( 'Access-Control-Allow-Credentials: true' );
			
			exit ( 0 );
		}
	}
	
	// ==================================================================
	//
	// Protected functions
	//
	// ------------------------------------------------------------------
	
	/**
	 * Parses the request to figure out the best format for response.
	 * Extension, if present, overrides the Accept header
	 *
	 * @throws RestException
	 * @return iFormat
	 * @example JsonFormat
	 */
	protected function negotiateResponseFormat() {
		$metadata = Util::nestedValue ( $this, 'apiMethodInfo', 'metadata' );
		// check if the api method insists on response format using @format comment
		
		if ($metadata && isset ( $metadata ['format'] )) {
			$formats = explode ( ',', ( string ) $metadata ['format'] );
			foreach ( $formats as $i => $f ) {
				$f = trim ( $f );
				if (! in_array ( $f, $this->formatOverridesMap ))
					throw new RestException ( 500, "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first." );
				$formats [$i] = $f;
			}
			call_user_func_array ( array (
					$this,
					'setSupportedFormats' 
			), $formats );
		}
		
		// check if client has specified an extension
		/**
		 * @var $format iFormat
		 */
		$format = null;
		$extensions = explode ( '.', parse_url ( $_SERVER ['REQUEST_URI'], PHP_URL_PATH ) );
		while ( $extensions ) {
			$extension = array_pop ( $extensions );
			$extension = explode ( '/', $extension );
			$extension = array_shift ( $extension );
			if ($extension && isset ( $this->formatMap [$extension] )) {
				$format = Scope::get ( $this->formatMap [$extension] );
				$format->setExtension ( $extension );
				// echo "Extension $extension";
				return $format;
			}
		}
		// check if client has sent list of accepted data formats
		if (isset ( $_SERVER ['HTTP_ACCEPT'] )) {
			$acceptList = Util::sortByPriority ( $_SERVER ['HTTP_ACCEPT'] );
			foreach ( $acceptList as $accept => $quality ) {
				if (isset ( $this->formatMap [$accept] )) {
					$format = Scope::get ( $this->formatMap [$accept] );
					$format->setMIME ( $accept );
					// echo "MIME $accept";
					// Tell cache content is based on Accept header
					@header ( 'Vary: Accept' );
					
					return $format;
				} elseif (false !== ($index = strrpos ( $accept, '+' ))) {
					$mime = substr ( $accept, 0, $index );
					if (is_string ( Defaults::$apiVendor ) && 0 === stripos ( $mime, 'application/vnd.' . Defaults::$apiVendor . '-v' )) {
						$extension = substr ( $accept, $index + 1 );
						if (isset ( $this->formatMap [$extension] )) {
							// check the MIME and extract version
							$version = intval ( substr ( $mime, 18 + strlen ( Defaults::$apiVendor ) ) );
							if ($version > 0 && $version <= $this->apiVersion) {
								$this->requestedApiVersion = $version;
								$format = Scope::get ( $this->formatMap [$extension] );
								$format->setExtension ( $extension );
								// echo "Extension $extension";
								Defaults::$useVendorMIMEVersioning = true;
								@header ( 'Vary: Accept' );
								
								return $format;
							}
						}
					}
				}
			}
		} else {
			// RFC 2616: If no Accept header field is
			// present, then it is assumed that the
			// client accepts all media types.
			$_SERVER ['HTTP_ACCEPT'] = '*/*';
		}
		if (strpos ( $_SERVER ['HTTP_ACCEPT'], '*' ) !== false) {
			if (false !== strpos ( $_SERVER ['HTTP_ACCEPT'], 'application/*' )) {
				$format = Scope::get ( 'JsonFormat' );
			} elseif (false !== strpos ( $_SERVER ['HTTP_ACCEPT'], 'text/*' )) {
				$format = Scope::get ( 'XmlFormat' );
			} elseif (false !== strpos ( $_SERVER ['HTTP_ACCEPT'], '*/*' )) {
				$format = Scope::get ( $this->formatMap ['default'] );
			}
		}
		if (empty ( $format )) {
			// RFC 2616: If an Accept header field is present, and if the
			// server cannot send a response which is acceptable according to
			// the combined Accept field value, then the server SHOULD send
			// a 406 (not acceptable) response.
			$format = Scope::get ( $this->formatMap ['default'] );
			$this->responseFormat = $format;
			throw new RestException ( 406, 'Content negotiation failed. ' . 'Try `' . $format->getMIME () . '` instead.' );
		} else {
			// Tell cache content is based at Accept header
			@header ( "Vary: Accept" );
			return $format;
		}
	}
	protected function negotiateCharset() {
		if (isset ( $_SERVER ['HTTP_ACCEPT_CHARSET'] )) {
			$found = false;
			$charList = Util::sortByPriority ( $_SERVER ['HTTP_ACCEPT_CHARSET'] );
			foreach ( $charList as $charset => $quality ) {
				if (in_array ( $charset, Defaults::$supportedCharsets )) {
					$found = true;
					Defaults::$charset = $charset;
					break;
				}
			}
			if (! $found) {
				if (strpos ( $_SERVER ['HTTP_ACCEPT_CHARSET'], '*' ) !== false) {
					// use default charset
				} else {
					throw new RestException ( 406, 'Content negotiation failed. ' . 'Requested charset is not supported' );
				}
			}
		}
	}
	protected function negotiateLanguage() {
		if (isset ( $_SERVER ['HTTP_ACCEPT_LANGUAGE'] )) {
			$found = false;
			$langList = Util::sortByPriority ( $_SERVER ['HTTP_ACCEPT_LANGUAGE'] );
			foreach ( $langList as $lang => $quality ) {
				foreach ( Defaults::$supportedLanguages as $supported ) {
					if (strcasecmp ( $supported, $lang ) == 0) {
						$found = true;
						Defaults::$language = $supported;
						break 2;
					}
				}
			}
			if (! $found) {
				if (strpos ( $_SERVER ['HTTP_ACCEPT_LANGUAGE'], '*' ) !== false) {
					// use default language
				} else {
					// ignore
				}
			}
		}
	}
	
	/**
	 * Filer api calls before authentication
	 */
	protected function preAuthFilter() {
		if (empty ( $this->filterClasses )) {
			return;
		}
		$this->dispatch ( 'preAuthFilter' );
		foreach ( $this->filterClasses as $filterClass ) {
			/**
			 *
			 * @var iFilter
			 */
			$filterObj = Scope::get ( $filterClass );
			
			if (! $filterObj instanceof iFilter) {
				throw new RestException ( 500, 'Filter Class ' . 'should implement iFilter' );
			} else if (! ($ok = $filterObj->__isAllowed ())) {
				if (is_null ( $ok ) && $filterObj instanceof iUseAuthentication) {
					// handle at authentication stage
					$this->postAuthFilterClasses [] = $filterClass;
					continue;
				}
				throw new RestException ( 403 ); // Forbidden
			}
		}
	}
	protected function authenticate() {
		$o = & $this->apiMethodInfo;
		$accessLevel = max ( Defaults::$apiAccessLevel, $o->accessLevel );
		try {
			if ($accessLevel || count ( $this->postAuthFilterClasses )) {
				$this->dispatch ( 'authenticate' );
				if (! count ( $this->authClasses )) {
					throw new RestException ( 403, 'at least one Authentication Class is required' );
				}
				foreach ( $this->authClasses as $authClass ) {
					$authObj = Scope::get ( $authClass );
					if (! method_exists ( $authObj, Defaults::$authenticationMethod )) {
						throw new RestException ( 500, 'Authentication Class ' . 'should implement iAuthenticate' );
					} elseif (! $authObj->{Defaults::$authenticationMethod} ()) {
						throw new RestException ( 401 );
					}
				}
				$this->authenticated = true;
			}
			$this->authVerified = true;
		} catch ( RestException $e ) {
			$this->authVerified = true;
			if ($accessLevel > 1) { // when it is not a hybrid api
				throw ($e);
			} else {
				$this->authenticated = false;
			}
		}
	}
	
	/**
	 * Filer api calls after authentication
	 */
	protected function postAuthFilter() {
		if (empty ( $this->postAuthFilterClasses )) {
			return;
		}
		$this->dispatch ( 'postAuthFilter' );
		foreach ( $this->postAuthFilterClasses as $filterClass ) {
			Scope::get ( $filterClass );
		}
	}
	protected function validate() {
		if (! Defaults::$autoValidationEnabled) {
			return;
		}
		$this->dispatch ( 'validate' );
		
		$o = & $this->apiMethodInfo;
		foreach ( $o->metadata ['param'] as $index => $param ) {
			$info = & $param [CommentParser::$embeddedDataName];
			if (! isset ( $info ['validate'] ) || $info ['validate'] != false) {
				if (isset ( $info ['method'] )) {
					$info ['apiClassInstance'] = Scope::get ( $o->className );
				}
				// convert to instance of ValidationInfo
				$info = new ValidationInfo ( $param );
				$validator = Defaults::$validatorClass;
				// if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) {
				// changed the above test to below for addressing this php bug
				// https://bugs.php.net/bug.php?id=53727
				if (function_exists ( "$validator::validate" )) {
					throw new \UnexpectedValueException ( '`Defaults::$validatorClass` must implement `iValidate` interface' );
				}
				$valid = $o->parameters [$index];
				$o->parameters [$index] = null;
				if (empty ( Validator::$exceptions ))
					$o->metadata ['param'] [$index] ['autofocus'] = true;
				$valid = $validator::validate ( $valid, $info );
				$o->parameters [$index] = $valid;
				unset ( $o->metadata ['param'] [$index] ['autofocus'] );
			}
		}
	}
	protected function call() {
		$this->dispatch ( 'call' );
		$o = & $this->apiMethodInfo;
		$accessLevel = max ( Defaults::$apiAccessLevel, $o->accessLevel );
		$object = Scope::get ( $o->className );
		switch ($accessLevel) {
			case 3 : // protected method
				$reflectionMethod = new \ReflectionMethod ( $object, $o->methodName );
				$reflectionMethod->setAccessible ( true );
				$result = $reflectionMethod->invokeArgs ( $object, $o->parameters );
				break;
			default :
				$result = call_user_func_array ( array (
						$object,
						$o->methodName 
				), $o->parameters );
		}
		$this->responseData = $result;
	}
	protected function compose() {
		$this->dispatch ( 'compose' );
		$this->composeHeaders ();
		/**
		 *
		 * @var iCompose Default Composer
		 */
		$compose = Scope::get ( Defaults::$composeClass );
		$this->responseData = is_null ( $this->responseData ) && Defaults::$emptyBodyForNullResponse ? '' : $this->responseFormat->encode ( $compose->response ( $this->responseData ), ! $this->productionMode );
	}
	public function composeHeaders(RestException $e = null) {
		// only GET method should be cached if allowed by API developer
		$expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0;
		if (! is_array ( Defaults::$headerCacheControl ))
			Defaults::$headerCacheControl = array (
					Defaults::$headerCacheControl 
			);
		$cacheControl = Defaults::$headerCacheControl [0];
		if ($expires > 0) {
			$cacheControl = $this->apiMethodInfo->accessLevel ? 'private, ' : 'public, ';
			$cacheControl .= end ( Defaults::$headerCacheControl );
			$cacheControl = str_replace ( '{expires}', $expires, $cacheControl );
			$expires = gmdate ( 'D, d M Y H:i:s \G\M\T', time () + $expires );
		}
		@header ( 'Cache-Control: ' . $cacheControl );
		@header ( 'Expires: ' . $expires );
		@header ( 'X-Powered-By: Luracast Restler v' . Restler::VERSION );
		
		if (Defaults::$crossOriginResourceSharing && isset ( $_SERVER ['HTTP_ORIGIN'] )) {
			header ( 'Access-Control-Allow-Origin: ' . (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER ['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin) );
			header ( 'Access-Control-Allow-Credentials: true' );
			header ( 'Access-Control-Max-Age: 86400' );
		}
		
		$this->responseFormat->setCharset ( Defaults::$charset );
		$charset = $this->responseFormat->getCharset () ?  : Defaults::$charset;
		
		@header ( 'Content-Type: ' . (Defaults::$useVendorMIMEVersioning ? 'application/vnd.' . Defaults::$apiVendor . "-v{$this->requestedApiVersion}" . '+' . $this->responseFormat->getExtension () : $this->responseFormat->getMIME ()) . '; charset=' . $charset );
		
		@header ( 'Content-Language: ' . Defaults::$language );
		
		if (isset ( $this->apiMethodInfo->metadata ['header'] )) {
			foreach ( $this->apiMethodInfo->metadata ['header'] as $header )
				@header ( $header, true );
		}
		$code = 200;
		if (! Defaults::$suppressResponseCode) {
			if ($e) {
				$code = $e->getCode ();
			} elseif (isset ( $this->apiMethodInfo->metadata ['status'] )) {
				$code = $this->apiMethodInfo->metadata ['status'];
			}
		}
		$this->responseCode = $code;
		@header ( "{$_SERVER['SERVER_PROTOCOL']} $code " . (isset ( RestException::$codes [$code] ) ? RestException::$codes [$code] : '') );
	}
	protected function respond() {
		$this->dispatch ( 'respond' );
		// handle throttling
		if (Defaults::$throttle) {
			$elapsed = time () - $this->startTime;
			if (Defaults::$throttle / 1e3 > $elapsed) {
				usleep ( 1e6 * (Defaults::$throttle / 1e3 - $elapsed) );
			}
		}
		if ($this->responseCode == 401) {
			$authString = count ( $this->authClasses ) ? Scope::get ( $this->authClasses [0] )->__getWWWAuthenticateString () : 'Unknown';
			@header ( 'WWW-Authenticate: ' . $authString, false );
		}
		echo $this->responseData;
		$this->dispatch ( 'complete' );
		exit ();
	}
	protected function message(Exception $exception) {
		$this->dispatch ( 'message' );
		
		if (! $exception instanceof RestException) {
			$exception = new RestException ( 500, $this->productionMode ? null : $exception->getMessage (), array (), $exception );
		}
		
		$this->exception = $exception;
		
		$method = 'handle' . $exception->getCode ();
		$handled = false;
		foreach ( $this->errorClasses as $className ) {
			if (method_exists ( $className, $method )) {
				$obj = Scope::get ( $className );
				if ($obj->$method ())
					$handled = true;
			}
		}
		if ($handled) {
			return;
		}
		if (! isset ( $this->responseFormat )) {
			$this->responseFormat = Scope::get ( 'JsonFormat' );
		}
		$this->composeHeaders ( $exception );
		/**
		 *
		 * @var iCompose Default Composer
		 */
		$compose = Scope::get ( Defaults::$composeClass );
		$this->responseData = $this->responseFormat->encode ( $compose->message ( $exception ), ! $this->productionMode );
		$this->respond ();
	}
	
	/**
	 * Provides backward compatibility with older versions of Restler
	 *
	 * @param int $version
	 *        	restler version
	 *        	
	 * @throws \OutOfRangeException
	 */
	public function setCompatibilityMode($version = 2) {
		if ($version <= intval ( self::VERSION ) && $version > 0) {
			require __DIR__ . "/compatibility/restler{$version}.php";
			return;
		}
		throw new \OutOfRangeException ();
	}
	
	/**
	 *
	 * @param int $version
	 *        	maximum version number supported
	 *        	by the api
	 * @param int $minimum
	 *        	minimum version number supported
	 *        	(optional)
	 *        	
	 * @throws InvalidArgumentException
	 * @return void
	 */
	public function setAPIVersion($version = 1, $minimum = 1) {
		if (! is_int ( $version ) && $version < 1) {
			throw new InvalidArgumentException ( 'version should be an integer greater than 0' );
		}
		$this->apiVersion = $version;
		if (is_int ( $minimum )) {
			$this->apiMinimumVersion = $minimum;
		}
	}
	
	/**
	 * Classes implementing iFilter interface can be added for filtering out
	 * the api consumers.
	 *
	 * It can be used for rate limiting based on usage from a specific ip
	 * address or filter by country, device etc.
	 *
	 * @param
	 *        	$className
	 */
	public function addFilterClass($className) {
		$this->filterClasses [] = $className;
	}
	
	/**
	 * protected methods will need at least one authentication class to be set
	 * in order to allow that method to be executed
	 *
	 * @param string $className
	 *        	of the authentication class
	 * @param string $resourcePath
	 *        	optional url prefix for mapping
	 */
	public function addAuthenticationClass($className, $resourcePath = null) {
		$this->authClasses [] = $className;
		$this->addAPIClass ( $className, $resourcePath );
	}
	
	/**
	 * Add api classes through this method.
	 *
	 * All the public methods that do not start with _ (underscore)
	 * will be will be exposed as the public api by default.
	 *
	 * All the protected methods that do not start with _ (underscore)
	 * will exposed as protected api which will require authentication
	 *
	 * @param string $className
	 *        	name of the service class
	 * @param string $resourcePath
	 *        	optional url prefix for mapping, uses
	 *        	lowercase version of the class name when
	 *        	not specified
	 *        	
	 * @return null
	 *
	 * @throws Exception when supplied with invalid class name
	 */
	public function addAPIClass($className, $resourcePath = null) {
		try {
			if ($this->productionMode && is_null ( $this->cached )) {
				$routes = $this->cache->get ( 'routes' );
				if (isset ( $routes ) && is_array ( $routes )) {
					$this->apiVersionMap = $routes ['apiVersionMap'];
					unset ( $routes ['apiVersionMap'] );
					Routes::fromArray ( $routes );
					$this->cached = true;
				} else {
					$this->cached = false;
				}
			}
			if (isset ( Scope::$classAliases [$className] )) {
				$className = Scope::$classAliases [$className];
			}
			if (! $this->cached) {
				$maxVersionMethod = '__getMaximumSupportedVersion';
				if (class_exists ( $className )) {
					if (method_exists ( $className, $maxVersionMethod )) {
						$max = $className::$maxVersionMethod ();
						for($i = 1; $i <= $max; $i ++) {
							$this->apiVersionMap [$className] [$i] = $className;
						}
					} else {
						$this->apiVersionMap [$className] [1] = $className;
					}
				}
				// versioned api
				if (false !== ($index = strrpos ( $className, '\\' ))) {
					$name = substr ( $className, 0, $index ) . '\\v{$version}' . substr ( $className, $index );
				} else if (false !== ($index = strrpos ( $className, '_' ))) {
					$name = substr ( $className, 0, $index ) . '_v{$version}' . substr ( $className, $index );
				} else {
					$name = 'v{$version}\\' . $className;
				}
				
				for($version = $this->apiMinimumVersion; $version <= $this->apiVersion; $version ++) {
					
					$versionedClassName = str_replace ( '{$version}', $version, $name );
					if (class_exists ( $versionedClassName )) {
						Routes::addAPIClass ( $versionedClassName, Util::getResourcePath ( $className, $resourcePath ), $version );
						if (method_exists ( $versionedClassName, $maxVersionMethod )) {
							$max = $versionedClassName::$maxVersionMethod ();
							for($i = $version; $i <= $max; $i ++) {
								$this->apiVersionMap [$className] [$i] = $versionedClassName;
							}
						} else {
							$this->apiVersionMap [$className] [$version] = $versionedClassName;
						}
					} elseif (isset ( $this->apiVersionMap [$className] [$version] )) {
						Routes::addAPIClass ( $this->apiVersionMap [$className] [$version], Util::getResourcePath ( $className, $resourcePath ), $version );
					}
				}
			}
		} catch ( Exception $e ) {
			$e = new Exception ( "addAPIClass('$className') failed. " . $e->getMessage (), $e->getCode (), $e );
			$this->setSupportedFormats ( 'JsonFormat' );
			$this->message ( $e );
		}
	}
	
	/**
	 * Add class for custom error handling
	 *
	 * @param string $className
	 *        	of the error handling class
	 */
	public function addErrorClass($className) {
		$this->errorClasses [] = $className;
	}
	
	/**
	 * Associated array that maps formats to their respective format class name
	 *
	 * @return array
	 */
	public function getFormatMap() {
		return $this->formatMap;
	}
	
	/**
	 * API version requested by the client
	 * 
	 * @return int
	 */
	public function getRequestedApiVersion() {
		return $this->requestedApiVersion;
	}
	
	/**
	 * When false, restler will run in debug mode and parse the class files
	 * every time to map it to the URL
	 *
	 * @return bool
	 */
	public function getProductionMode() {
		return $this->productionMode;
	}
	
	/**
	 * Chosen API version
	 *
	 * @return int
	 */
	public function getApiVersion() {
		return $this->apiVersion;
	}
	
	/**
	 * Base Url of the API Service
	 *
	 * @return string
	 *
	 * @example http://localhost/restler3
	 * @example http://restler3.com
	 */
	public function getBaseUrl() {
		return $this->baseUrl;
	}
	
	/**
	 * List of events that fired already
	 *
	 * @return array
	 */
	public function getEvents() {
		return $this->events;
	}
	
	/**
	 * Magic method to expose some protected variables
	 *
	 * @param string $name
	 *        	name of the hidden property
	 *        	
	 * @return null|mixed
	 */
	public function __get($name) {
		if ($name {0} == '_') {
			$hiddenProperty = substr ( $name, 1 );
			if (isset ( $this->$hiddenProperty )) {
				return $this->$hiddenProperty;
			}
		}
		return null;
	}
	
	/**
	 * Store the url map cache if needed
	 */
	public function __destruct() {
		if ($this->productionMode && ! $this->cached) {
			$this->cache->set ( 'routes', Routes::toArray () + array (
					'apiVersionMap' => $this->apiVersionMap 
			) );
		}
	}
	
	/**
	 * pre call
	 *
	 * call _pre_{methodName)_{extension} if exists with the same parameters as
	 * the api method
	 *
	 * @example _pre_get_json
	 *         
	 */
	protected function preCall() {
		$o = & $this->apiMethodInfo;
		$preCall = '_pre_' . $o->methodName . '_' . $this->requestFormat->getExtension ();
		
		if (method_exists ( $o->className, $preCall )) {
			$this->dispatch ( 'preCall' );
			call_user_func_array ( array (
					Scope::get ( $o->className ),
					$preCall 
			), $o->parameters );
		}
	}
	
	/**
	 * post call
	 *
	 * call _post_{methodName}_{extension} if exists with the composed and
	 * serialized (applying the repose format) response data
	 *
	 * @example _post_get_json
	 */
	protected function postCall() {
		$o = & $this->apiMethodInfo;
		$postCall = '_post_' . $o->methodName . '_' . $this->responseFormat->getExtension ();
		if (method_exists ( $o->className, $postCall )) {
			$this->dispatch ( 'postCall' );
			$this->responseData = call_user_func ( array (
					Scope::get ( $o->className ),
					$postCall 
			), $this->responseData );
		}
	}
}
